import { NextFunction, Request, Response } from 'express'
import dayjs from 'dayjs'
import set from 'lodash/set'
import { EContainerServiceName } from '@/types/container'
import { MongoEntityMixin } from '../mixins'
import UserToken from './UserToken'
import User from '../mysql/User'
import UserService from '../mysql/UserService'
import UserAuthError from '@/errors/UserAuthError'
import { getUserToken, replaceReqUserToken } from '@/utils'
import { getContainer } from '@/container/utils'
import { expiredAtFormate, refreshUserToken } from '@/utils/jwt'

type ITokenValidateQueueCall = (
  userToken: UserToken | null,
  userInfo: User | null,
  newUserToken: UserToken | null
) => void

// 携带了存储库实例的服务类
export default class UserTokenService extends MongoEntityMixin(UserToken) {
  // token 刷新状态 Map 用于防止重复刷新 key 为 token 字符串，value 为 token 刷新状态
  private static tokenValidateStatusMap = new Map<string, boolean>()

  // token 刷新队列 Map 用于防止重复刷新,当 token 正在刷新时，其他请求会被加入队列等待token刷新完成后再携带新的token对象继续请求
  private static tokenValidateQueueMap = new Map<string, ITokenValidateQueueCall[]>()

  // 使用户 token 过期
  static async expiredUserToken(token: string) {
    await this.getRepository().updateOne(
      { token, is_blacked: false },
      { $set: { token_expired_at: dayjs().subtract(1, 'm').format(expiredAtFormate) } }
    )
  }

  // 拉黑用户 token 一般用于登出，登出时会将 token 加入黑名单
  // 一般登出可以放入队列中去执行，防止并发请求导致的异常
  static async blackedUserToken(token: string) {
    await this.getRepository().updateOne({ token, is_blacked: false }, { $set: { is_blacked: true } })
  }

  // 根据 token 获取用户 token 信息
  static async getTokenByTokenStr(token: string) {
    if (!token) {
      return null
    }
    return await this.getRepository().findOneBy({ token })
  }

  // 校验 token 是否有效
  static async validateToken(req: Request, res: Response, next: NextFunction) {
    // 获取请求头中的 token
    const token = getUserToken(req)
    if (!token) {
      throw new UserAuthError('请登录后再操作')
    }
    // 校验函数
    const validateQueueCall: ITokenValidateQueueCall = (userToken, userInfo, newUserToken) => {
      try {
        // 如果没有token 则返回401异常提醒登录 这里不判断is_blacked，因为在这里执行之前可能已经被拉黑了
        if (!userToken || userToken.is_refresh_token_expired) {
          throw new UserAuthError('您的登录有效期已过，请重新登录')
        }
        // 将用户信息绑定到请求上
        set(req, 'user', userInfo)
        if (!userInfo) {
          throw new UserAuthError('用户信息异常，需要重新登录')
        }
        // 过期并且有新的token
        if (userToken.is_expired && !newUserToken) {
          throw new UserAuthError('您的登录有效期已过，请重新登录')
        }
        // 有新的token 则替换
        if (newUserToken) {
          // 覆盖原来请求头中的token
          replaceReqUserToken(newUserToken.token, req)
          // 将新的token设置到响应头中 Token Replaced By
          res.setHeader('T-Replaced-By', newUserToken.token)
        }
        // 因为上下文在外部执行过一次，如果这里不重新设置一次，会导致后续的请求中获取到的是上一次的请求设置的值，因为队列中的函数会在并发执行
        // 比如同时有两个请求，第一个请求执行完毕后，第二个请求执行时，上下文中的值是第一个请求的值
        // 第二次请求放在了第一次请求的队列中，所以执行第二次请求的队列实际上是在第一次请求的上下文中执行的
        // 所以这里需要重新设置一次上下文
        getContainer().overrideStoreValue(EContainerServiceName.REQUEST, req)
        getContainer().overrideStoreValue(EContainerServiceName.RESPONSE, res)
        next()
      } catch (e) {
        next(e)
      }
    }
    // 直接把校验函数加入队列头部 等待执行
    const queueList = this.tokenValidateQueueMap.get(token)
    if (queueList) {
      queueList.unshift(validateQueueCall)
    } else {
      this.tokenValidateQueueMap.set(token, [validateQueueCall])
    }
    // 当前token不在校验
    if (!this.tokenValidateStatusMap.get(token)) {
      // 当前token不在刷新中，设置为刷新中
      this.tokenValidateStatusMap.set(token, true)
      // 先获取需要的公用信息，然后执行队列中的请求
      try {
        // 获取用户 token 信息
        let userToken: UserToken | null = null
        // 用户信息
        let userInfo: User | null = null
        // 新的 token 对象
        let newUserToken: UserToken | null = null
        userToken = await this.getTokenByTokenStr(token)
        if (userToken && !userToken.is_blacked && !userToken.is_refresh_token_expired) {
          // 查找用户信息
          userInfo = await UserService.getUserInfoByUserID(userToken.user_id)
          if (userInfo && userToken.is_expired) {
            // 获取到新的token对象
            newUserToken = await refreshUserToken(userToken)
          }
        }
        // 开始 执行队列中的请求 直到队列为空
        let queueCall = this.tokenValidateQueueMap.get(token)?.pop()
        while (queueCall) {
          queueCall(userToken, userInfo, newUserToken)
          queueCall = this.tokenValidateQueueMap.get(token)?.pop()
        }
      } catch (error) {
        // 异常则执行
        next(error)
      } finally {
        // 无论成功还是失败，都要将token从刷新中状态中移除
        this.tokenValidateStatusMap.delete(token)
        // 清除token刷新队列
        this.tokenValidateQueueMap.delete(token)
      }
    }
  }
}
